/**
 * 实现对 MQTT 和在线返回的聊天消息的解析
 */
import { getLogger, asType, truthy } from '@gnlab/utils'
import pick from 'lodash/pick'
import { account$ } from '@/services/auth/info'
import type { RecvMessage, HookData } from '@/services/common/qywx-mqtt'
import { parseRecvMessageData } from '@/services/common/qywx-mqtt/protocol'
import {
  detectIsNormal,
  detectIsGroup,
  detectIsNotice,
  makeContactId,
  splitPeopleConversationId,
  makeConversationId,
} from '@/services/contacts'
import type { Contact } from '@/services/contacts'
import type {
  ChatRecord,
  BaseChatRecord,
  SendChatRecord,
  RecvChatRecord,
  ContentTypeMap,
  ParseFailedContent,
  RawGroupMemberUpdateContent,
  RawLinkCardContent,
  MiniProgramContent,
  RawPersonCardContent,
  SendAndRecvChatRecord,
  RawCallContent,
} from './base'
import { ChatRecordType, contentFormatters } from './base'

const logger = getLogger('services/chat')

/**
 * ================================
 * 基本类型定义
 * ================================
 */

/**
 * 原始的聊天记录内容
 * （没有包含各类消息的内容相关的字段）
 */
export type RawChatRecord = {
  conversation_id: string
  receiver: string // 消息接收者的 user_id, 对于群，等于 conversation_id
  sender: string // 消息发送者的 user_id，对于群就是群内用户的 user_id
  sender_name: string // 消息发送者的昵称，对于群就是群内用户的昵称（而不是群名）
  server_id: string // 消息在腾讯服务器端中的 id（发送引用消息时会用到）

  // 后端附加的字段
  isOpenApi?: boolean // 消息是否通过 openAPI 发送（仅限发送消息）
  workflowName?: string // 消息是否通过私聊工作流发送（仅限发送消息）
  hit_node?: string // 机器人消息信息（有值说明是机器人发送的消息） hit_title 同理
  hit_title?: string
  sendUserId?: string // 若是其他账号发送的消息（例如转交、观察者模式），此为发送者的账号ID
  sendUserName?: string // 若是其他账号发送的消息（例如转交、观察者模式），此为发送者的账号名称

  // 未用到的字段，不包含在定义里，避免误用
  // content_type: 2 | 0,            // 貌似 2 是收到消息，0 是自己发送的消息
  // is_pc: 0,
  // send_time: string,
}

/**
 * 聊天消息的 HookData 类型
 * 后段会额外附加一些字段，例如最终会变成这样：{ type: 11041, data: {...}, isRecall: true }
 */
export interface ChatHookData<T> extends HookData<T> {
  id?: string // 消息发送成功后 MQTT 推送的回执会包含此字段，对应之前临时生成的“发送凭证 ID”
  matchWarnRules?: string[] // 若收到的聊天消息触发了预警，此为被触发的规则名称列表
  IsRecall?: boolean // 消息是否已撤销（仅限发送消息。目前只有线上返回的聊天内容会带此字段）
}

/**
 * 经过初步格式化的聊天推送消息（与 MQTT 推送和线上返回的都不完全相同，须经过格式化才是此格式）
 */
export type RawChatMessage<T> = Pick<
  RecvMessage<ChatHookData<T>>,
  'msgId' | 'wxid' | 'timestamp' | 'type' | 'data'
>

/**
 * 从聊天消息中提取出的联系人信息
 */
export type ContactFromChat = Pick<Contact, 'id' | 'ownerWxid' | 'conversationId' | 'userId'> & {
  name?: string
}

/**
 * ================================
 * 基本解析函数
 * ================================
 */

/**
 * 解析聊天消息
 * 若 message 不是聊天消息，返回 null
 *
 * irregularFormatter: 如果 message 已经经过了 irregularFormatter 的预处理，此处可传入 null 以避免二次处理
 * contentParser: 可以传入一个自定义的内容解析函数
 *
 */
export function parseChatRecord(
  rawMessage: RawChatMessage<unknown>,
  irregularFormatter: typeof formatIrregularMessage | null = formatIrregularMessage,
  contentParser = parseChatContent,
): ChatRecord | null {
  const message = (
    irregularFormatter ? irregularFormatter(rawMessage) : rawMessage
  ) as RawChatMessage<RawChatRecord> | null
  if (!message) return null

  const wxid = message.wxid!
  const { data: rawRecord } = message.data

  const contentResult = contentParser(message)
  if (!contentResult) return null

  // 标记已明确类型的变量
  if (!asType<RawChatMessage<RawChatRecord>>(message)) return null
  if (!asType<RawChatRecord>(rawRecord)) return null

  const baseRecord = formatBaseChatRecord(message, contentResult.type, contentResult.content)
  const isSend = rawRecord.sender === wxid
  const record = isSend
    ? formatSendRecord(baseRecord, message)
    : formatRecvRecord(baseRecord, message)
  return record as ChatRecord
}

function formatBaseChatRecord(
  message: RawChatMessage<RawChatRecord>,
  contentType: ChatRecordType,
  content: ContentTypeMap[ChatRecordType],
): BaseChatRecord<ChatRecordType> {
  const { msgId, timestamp, data } = message
  return {
    id: msgId,
    serverId: data.data.server_id,
    timestamp,
    type: contentType,
    content,
  }
}
function formatSendRecord(
  base: BaseChatRecord<ChatRecordType>,
  message: RawChatMessage<RawChatRecord>,
): SendChatRecord<ChatRecordType> {
  const accountId = account$.value?.id ?? ''
  const rawRecord = message.data.data

  // 对于回执消息，根据 server_id 是否有值判断原消息是否发送成功
  // 对于其他情况，始终认为发送成功
  const receiptId = message.data.id
  const success = truthy(receiptId) ? truthy(rawRecord.server_id) : true

  const byRobot =
    rawRecord.hit_node ?? ''
      ? { hit_node: rawRecord.hit_node!, hit_title: rawRecord.hit_title! }
      : null
  const byOpenAPI = rawRecord.isOpenApi ?? false
  const byChatFlow = rawRecord.workflowName ?? null
  const { IsRecall: isRecall } = message.data
  const sendByOtherAccount = truthy(rawRecord.sendUserId) && rawRecord.sendUserId !== accountId
  const accountName = sendByOtherAccount ? { accountName: rawRecord.sendUserName ?? '' } : {}

  return {
    ...base,
    ...(receiptId ?? '' ? { receiptId } : {}),
    isSend: true,
    byRobot,
    byOpenAPI,
    byChatFlow,
    success,
    isRecall,
    ...accountName,
  }
}
function formatRecvRecord(
  base: BaseChatRecord<ChatRecordType>,
  message: RawChatMessage<RawChatRecord>,
): RecvChatRecord<ChatRecordType> {
  const { matchWarnRules = [] } = message.data // 目前只有“接收”的“文本消息”可能匹配上预警规则
  const rawRecord: RawChatRecord = message.data.data
  const isGroup = detectIsGroup(rawRecord.conversation_id)
  return {
    ...base,
    isSend: false,
    matchWarnRules,
    groupSenderWxid: isGroup ? rawRecord.sender : '',
    groupSenderName: isGroup ? rawRecord.sender_name : '',
  }
}

/**
 * 转换 message 中不符合规则的 chat record 内容（部分消息类型的 chat record 与标注的不同）
 *
 * - 若不需转换，原样返回
 * - 返回 null 代表消息无法完成转换，将其忽略
 */
function formatIrregularMessage(
  message: RawChatMessage<unknown>,
): RawChatMessage<RawChatRecord> | null {
  const { type, data: origin } = message.data

  let record: RawChatRecord | null = null
  if (type === 11072 || type === 11073) {
    // 群拉人、群踢人
    const raw = origin as RawGroupMemberUpdateContent
    record = {
      conversation_id: raw.room_conversation_id,
      receiver: raw.room_conversation_id,
      server_id: message.msgId,
      sender_name: raw.op_user_name,
      sender: raw.op_user_id,
      ...raw,
    }
  }
  if (type === 11237) {
    // 语音视频
    const raw = origin as RawCallContent | RawChatRecord
    if (!('conversation_id' in raw)) {
      const contactUserId = raw.inviteMsg.inviteId
      if (typeof contactUserId !== 'string') return null
      record = {
        conversation_id: makeConversationId({
          type: null,
          ownerWxid: message.wxid ?? '',
          contactUserId,
        }),
        receiver: message.wxid ?? '',
        server_id: message.msgId,
        sender_name: '',
        sender: contactUserId,
      }
    }
  }

  return record
    ? { ...message, data: { ...message.data, data: record } }
    : (message as RawChatMessage<RawChatRecord>)
}

/**
 * 确认聊天消息内容的类型，并返回格式化过的类型
 * 若 message 不是有效的聊天消息，返回 null
 */
export function parseChatContent(
  message: RawChatMessage<RawChatRecord>,
): { type: ChatRecordType; content: ContentTypeMap[ChatRecordType] } | null {
  const { type, data: rawData } = message.data

  const recordType = {
    11041: ChatRecordType.Text,
    11042: ChatRecordType.Image,
    11043: ChatRecordType.Video,
    11044: ChatRecordType.Audio,
    11045: ChatRecordType.File,
    11048: ChatRecordType.Emotion,
    11047: ChatRecordType.LinkCard,
    11050: ChatRecordType.PersonCard,
    11066: ChatRecordType.MiniProgram,
    11124: ChatRecordType.Videonum,
    11046: ChatRecordType.Position,
    11049: ChatRecordType.RedPocket,
    11072: ChatRecordType.GroupAdd,
    11073: ChatRecordType.GroupRemove,
    11187: ChatRecordType.ContactNotify,
    11237: ChatRecordType.Call,
  }[type] as Exclude<ChatRecordType, ChatRecordType.ParseFailed> | undefined

  // 用一个范例 formatter 标注类型，不然所有种类信息会合并在一起，怎么标注都报错
  type Formatter = (typeof contentFormatters)[ChatRecordType.Text]

  if (recordType !== undefined) {
    try {
      const content = (contentFormatters[recordType] as Formatter)(
        rawData as unknown as Parameters<Formatter>[0],
      )
      return { type: recordType, content }
    } catch (e) {
      return {
        type: ChatRecordType.ParseFailed,
        content: {
          message: `消息内容格式化失败：${(e as Error).toString()}`,
        } as ParseFailedContent,
      }
    }
  } else if (
    type === 11051 &&
    typeof (rawData as Record<string, unknown>)['appinfo'] === 'string'
  ) {
    // 11051 都是底层尚不支持的消息类型，但根据 appinfo，能识别出一些类型来
    const unknownMsgData = rawData as unknown as { appinfo: string }
    if (unknownMsgData.appinfo.startsWith('c2b_pay_')) {
      return {
        type: ChatRecordType.TransferMoney,
        content: contentFormatters[ChatRecordType.TransferMoney](unknownMsgData),
      }
    }
  }

  return null
}

/**
 * 从聊天消息中提取出联系人信息
 * 若消息对象不是有效的联系人，返回 null
 *
 * 需保证 rawRecord 是从经过 irregularFormatter 的 message 里取出来的，
 * 不然对于 11072 等特殊格式的消息，会报错
 */
export function parseChatContact(wxid: string, rawRecord: RawChatRecord): ContactFromChat | null {
  if (!detectIsNormal(rawRecord.conversation_id)) return null

  const isSend = rawRecord.sender === wxid
  const isGroup = detectIsGroup(rawRecord.conversation_id)
  const isNotice = detectIsNotice(rawRecord.conversation_id)
  const userId =
    isGroup || isNotice ? rawRecord.conversation_id : isSend ? rawRecord.receiver : rawRecord.sender
  const name = !isSend && !isGroup ? { name: rawRecord.sender_name } : {}

  return {
    id: makeContactId(wxid, userId),
    ownerWxid: wxid,
    conversationId: rawRecord.conversation_id,
    userId,
    ...name,
  }
}

/**
 * ================================
 * 处理 MQTT 推送的聊天消息
 * ================================
 */

export type MQTTChatMessage = RecvMessage<ChatHookData<RawChatRecord>>

/**
 * 解析 MQTT 推送的聊天消息及其中的联系人信息
 * 若聊天消息或联系人未能解析出来，对应的字段值为 null
 */
export function parseMQTTChatRecordAndContact(message: MQTTChatMessage): {
  record: ChatRecord | null
  contact: ContactFromChat | null
} {
  const formattedMessage = formatIrregularMessage(message)
  if (!formattedMessage) return { record: null, contact: null }

  const record = parseChatRecord(formattedMessage)
  if (!record) return { record: null, contact: null }

  const contact = parseChatContact(formattedMessage.wxid!, formattedMessage.data.data)
  return { record, contact }
}

/**
 * ================================
 * 处理在线返回的聊天消息
 * ================================
 */

export interface OnlineChatMessage {
  conversation_id: string
  hostName: string
  msg: string
  msgId: string
  timestamp: number
  type: 30002
  userId: string
  wxid: string
}

export function parseOnlineChatRecord(message: OnlineChatMessage): ChatRecord {
  try {
    const rawChatMessage = transformOnlineChatMessage(message)
    const record = parseChatRecord(rawChatMessage, formatIrregularOnlineMessage)
    if (record) return record

    logger.warn('云端记录中发现不支持的聊天内容', message)
    return makeParseFailedChatRecord('不支持的聊天内容', rawChatMessage)
  } catch (e) {
    logger.warn('解析云端聊天记录失败', e, message)
    return makeParseFailedChatRecord(String(e))
  }
}

/**
 * 把接口返回的聊天记录消息转换统一格式
 */
function transformOnlineChatMessage(raw: OnlineChatMessage): RawChatMessage<unknown> {
  let data = parseRecvMessageData(raw.msg)
  if (typeof data === 'string') data = parseRecvMessageData(data) // 历史遗留的聊天记录里，有些 data 可能多 JSON 化了一次

  return {
    msgId: raw.msgId,
    timestamp: raw.timestamp,
    wxid: raw.wxid,
    type: raw.type,
    data: data as ChatHookData<unknown>,
  }
}

/**
 * 处理历史遗留的或不标准的聊天记录格式
 */
export function formatIrregularOnlineMessage(
  message: RawChatMessage<unknown>,
): RawChatMessage<RawChatRecord> | null {
  const wxid = message.wxid!
  const {
    type: messageType,
    msgId,
    data: { data: origin },
  } = message

  // ===== 处理部分消息类型的老版本数据格式 =====

  // 企微 4.0 升级前的视频号格式
  if (messageType === 30002 && message.data.type === 11117) {
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { image_url, logo_url, title, videonum_data, videonum_url, internalPath, ...rest } =
      message.data.data as Record<string, unknown>
    return formatIrregularMessage({
      ...message,
      data: {
        ...message.data,
        type: 11124,
        data: {
          ...rest,
          cover_url: image_url,
          desc: title,
          feed_type: 0,
          nickname: title,
          thumb_url: image_url,
          url: videonum_url,
          extras: videonum_data,
          avatar: logo_url,
        },
      },
    })
  }

  // ===== 处理不标准的聊天记录类型 =====

  // 接收图片
  if (messageType === 30002 && message.data.type === 11044) {
    const data = message.data as ChatHookData<Record<string, unknown>> & { speechResult?: string }
    return formatIrregularMessage({
      ...message,
      data: {
        ...data,
        data: {
          ...data.data,
          text: data.speechResult,
        },
      },
    })
  }

  // ===== 处理历史遗留的聊天记录格式 =====

  let recordType: number | null = null
  let record: (RawChatRecord & Record<string, unknown>) | null = null

  function commonPadRecord(extra: Record<string, unknown>) {
    return {
      conversation_id: (origin as { conversation_id: string }).conversation_id,
      receiver: '',
      sender: wxid,
      sender_name: '',
      server_id: msgId,
      ...extra,
    } as RawChatRecord
  }

  if (messageType === 11029) {
    // 发送文本
    const raw = origin as { conversation_id: string; IsRobot: boolean; content: string }
    recordType = 11041
    record = commonPadRecord({ content: raw.content })
  } else if (messageType === 11030) {
    // 发送图片
    const raw = origin as { conversation_id: string; IsRobot: boolean; file: string }
    recordType = 11042
    record = commonPadRecord({ file_path: raw.file })
  } else if (messageType === 11067) {
    // 发送视频
    const raw = origin as { file: string }
    recordType = 11043
    record = commonPadRecord({ file_path: raw.file })
  } else if (messageType === 11031) {
    // 发送文件
    const raw = origin as { file: string }
    recordType = 11045
    record = commonPadRecord({ file_path: raw.file })
  } else if (messageType === 11070) {
    // 发送动画表情
    const raw = origin as { file: string }
    recordType = 11048
    record = commonPadRecord({ file_path: raw.file })
  } else if (messageType === 11033) {
    // 发送链接卡片
    const raw = origin as RawLinkCardContent
    recordType = 11047
    record = commonPadRecord(pick(raw, 'desc', 'image_url', 'title', 'url'))
  } else if (messageType === 11065) {
    // 发送小程序
    const raw = origin as MiniProgramContent
    recordType = 11066
    record = commonPadRecord(
      pick(raw, 'username', 'appid', 'appname', 'appicon', 'title', 'page_path', 'cover_path'),
    )
  } else if (messageType === 11069) {
    // openapi 发送群 @
    const raw = origin as { at_list?: string[]; content: string }
    recordType = 11041
    const content = (raw.at_list ?? []).map(v => `@${v}`).join(' ') + ' ' + raw.content
    record = commonPadRecord({ content })
  } else if (messageType === 11034) {
    // openapi 发送名片
    const raw = origin as RawPersonCardContent
    recordType = 11050
    record = commonPadRecord(pick(raw, 'avatar', 'nickname', 'user_id'))
  } else if (messageType === 30004) {
    // 机器人发文本
    const raw = origin as {
      conversation_id: string
      content: string
      hit_node?: string
      hit_title?: string
    }
    const receiver = getRobotReceiver(raw.conversation_id, wxid)
    if (receiver) {
      // 忽略无法解析出 receiver 的消息
      recordType = 11041
      record = commonPadRecord(pick(raw, 'content', 'hit_node', 'hit_title'))
    }
  } else if (messageType === 30005) {
    // 机器人发图片
    const raw = origin as {
      conversation_id: string
      file: string
      hit_node?: string
      hit_title?: string
    }
    const receiver = getRobotReceiver(raw.conversation_id, wxid)
    if (receiver) {
      // 忽略无法解析出 receiver 的消息
      recordType = 11042
      record = commonPadRecord({
        file_path: raw.file,
        hit_node: raw.hit_node,
        hit_title: raw.hit_title,
      })
    }
  }

  return record
    ? { ...message, type: 30002, data: { type: recordType!, data: record } }
    : formatIrregularMessage(message)
}

function getRobotReceiver(conversationId: string, selfWxid: string) {
  if (detectIsGroup(conversationId)) return conversationId
  const [id1, id2] = splitPeopleConversationId(conversationId)
  return id1 === selfWxid ? id2 : id1
}

/**
 * 生成一条“解析失败”的聊天记录
 */
export function makeParseFailedChatRecord(
  message: string,
  raw?: RawChatMessage<unknown>,
): SendAndRecvChatRecord<ChatRecordType.ParseFailed> {
  // 尽可能提取消息内容，以还原消息原貌
  const extracted: Partial<{
    type: number
    sender: string
    server_id: string
    msgId: string
    timestamp: number
    wxid: string
    appinfo: string
  }> = {
    ...pick(raw ?? {}, 'msgId', 'timestamp', 'wxid'),
    ...pick(raw?.data ?? {}, 'type'),
    ...pick(raw?.data.data ?? {}, 'sender', 'server_id', 'appinfo'),
  }

  const isSend =
    truthy(extracted.wxid) && truthy(extracted.sender) ? extracted.wxid === extracted.sender : true
  const common = {
    type: ChatRecordType.ParseFailed as const,
    id: extracted.msgId ?? Math.round(Math.random() * 1000000).toString(),
    serverId: extracted.server_id ?? '',
    content: { message, type: extracted.type, appinfo: extracted.appinfo },
    timestamp: extracted.timestamp ?? new Date().getTime(),
  }
  if (isSend) {
    return {
      ...common,
      isSend,
      byRobot: null,
      byOpenAPI: false,
      byChatFlow: null,
      success: true,
    }
  } else {
    return {
      ...common,
      isSend,
      matchWarnRules: [],
      groupSenderWxid: '',
      groupSenderName: '',
    }
  }
}

/**
 * ================================
 * 处理随联系人列表返回的每个联系人的最近一条聊天消息
 * ================================
 */
export function parseContactLatestChatRecord(raw: {
  conversationId: string
  ownerWxid: string
  userId: string
  message: string
}): ChatRecord | null {
  try {
    const rawChatMessage = transformOnlineChatMessage({
      conversation_id: raw.conversationId,
      hostName: '',
      msg: raw.message,
      msgId: '',
      timestamp: 0,
      type: 30002 as const,
      userId: raw.userId,
      wxid: raw.ownerWxid,
    })
    const record = parseChatRecord(rawChatMessage, formatIrregularOnlineMessage)
    if (record) return record

    logger.warn('联系人信息中发现不支持的聊天内容', rawChatMessage)
    return makeParseFailedChatRecord('不支持的聊天内容', rawChatMessage)
  } catch (e) {
    logger.warn('联系人信息中的聊天记录解析失败', e, raw.message)
    return makeParseFailedChatRecord(String(e))
  }
}
